﻿// --------------------------------------------------------------------------------------------------------------------- 
// <copyright file="ExpressionCalculator.cs" company="Scrum for Team System">
//   None
// </copyright>
// <summary>
//   Defines the ExpressionCalculator type.
// </summary>
// ---------------------------------------------------------------------------------------------------------------------

namespace ScrumForTeamSystem.EventService.TransitionService
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.Linq;

    using Microsoft.TeamFoundation.WorkItemTracking.Client;

    /// <summary>
    /// Defines the ExpressionCalculator type.
    /// </summary>
    public class ExpressionCalculator
    {
        /// <summary>
        /// The expressions hash table.
        /// </summary>
        private readonly Dictionary<string, Func<WorkItem, object>> expressions =
                new Dictionary<string, Func<WorkItem, object>>();

        /// <summary>
        /// The operators hash table.
        /// </summary>
        private static readonly Dictionary<string, Func<double?, double?, double?>> operatorMap =
                new Dictionary<string, Func<double?, double?, double?>>
                    {
                            { "+", (d1, d2) => d1 + d2 },
                            { "-", (d1, d2) => d1 - d2 },
                            { "/", (d1, d2) => d1.HasValue && d2.HasValue ? d1 / d2 : null },
                            { "*", (d1, d2) => d1 * d2 }
                    };

        /// <summary>
        /// The rounding function.
        /// </summary>
        private static readonly Func<double?, int?, object> round = (d, i) => i.HasValue && d.HasValue ? Math.Round(d.Value, i.Value) : d;

        /// <summary>
        /// The singleton instance field.
        /// </summary>
        private static ExpressionCalculator instance;

        /// <summary>
        /// Gets Instance.
        /// </summary>
        /// <value>
        /// The instance.
        /// </value>
        public static ExpressionCalculator Instance
        {
            get
            {
                return instance = instance ?? new ExpressionCalculator();
            }
        }

        /// <summary>
        /// Gets the expression calculation.
        /// </summary>
        /// <param name="expressionValue">The expression string.</param>
        /// <param name="decimalPlaces">The depth of decimal places.</param>
        /// <returns>The expression as a function.</returns>
        public Func<WorkItem, object> GetExpressionMethod(string expressionValue, int? decimalPlaces)
        {
            if (expressionValue == null)
            {
                throw new ArgumentNullException("expressionValue");
            }

            Func<WorkItem, object> expression;

            var trimmedExpression = expressionValue.Trim();

            if (string.IsNullOrEmpty(trimmedExpression))
            {
                throw new ArgumentNullException(expressionValue);
            }

            var compoundKey
                     = string.Concat(trimmedExpression, decimalPlaces);

            if (!this.expressions.TryGetValue(compoundKey, out expression))
            {
                // if we have a multi part expression; create the calculator method
                if (trimmedExpression.Contains(" "))
                {
                    List<Func<WorkItem, double?>> values;
                    List<Func<double?, double?, double?>> operators;

                    if (!TryGetValuesAndOperators(trimmedExpression, out values, out operators))
                    {
                        throw new ArgumentException(trimmedExpression);
                    }

                    expression = w =>
                                 {
                                     var output = values.First()(w);
                                     var skip = 0;

                                     foreach (var @operator in operators)
                                     {
                                         skip++;
                                         output = @operator(output, values.Skip(skip).First()(w));
                                     }

                                     return round(output, decimalPlaces);
                                 };
                }
                else
                {
                    // Otherwise use the expression as a field name
                    expression = w => w.Fields[trimmedExpression].Value;
                }

                this.expressions.Add(compoundKey, expression);
            }

            return expression;
        }

        /// <summary>
        /// Tries the get values and operators.
        /// </summary>
        /// <param name="expressionValue">The expression string.</param>
        /// <param name="values">The values.</param>
        /// <param name="operators">The operators.</param>
        /// <returns><c>True</c> if the correct number of value and operators are found; otherwise <c>false</c>.</returns>
        private static bool TryGetValuesAndOperators(string expressionValue, out List<Func<WorkItem, double?>> values, out List<Func<double?, double?, double?>> operators)
        {
            var elements = expressionValue.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);

            values = new List<Func<WorkItem, double?>>();
            operators = new List<Func<double?, double?, double?>>();
            
            for (int i = 0; i < elements.Length; i++)
            {
                var element = elements[i];

                if (i % 2 != 0)
                {
                    Func<double?, double?, double?> @operator;
                    if (!operatorMap.TryGetValue(element, out @operator))
                    {
                        throw new ArgumentException(expressionValue);
                    }

                    operators.Add(@operator);
                }
                else
                {
                    // Is this value a number or a field name?
                    double value;
                    if (double.TryParse(element, out value))
                    {
                        values.Add(w => value);
                    }
                    else
                    {
                        values.Add(w => Convert.ToDouble(w.Fields[element].Value, CultureInfo.InvariantCulture));
                    }
                }
            }

            return values.Count() != 0 && values.Count() > operators.Count();
        }
    }
}